C++ private 纯虚函数:访问性与多态无关
阅读标准库源码时会在 std::pmr::memory_resource 里看到一种乍看矛盾的写法:三个纯虚函数 do_allocate、do_deallocate、do_is_equal 都声明在 private 区。基类的 private 成员派生类无权访问,那派生类怎么"实现"一个自己看不见的纯虚函数?这样的声明为什么成立、又为什么要这样设计?本文用一个最小示例验证结论:虚分派与访问性完全无关,private 虚函数是成立且常用的设计。
标准库中的实例
std::pmr::memory_resource 的公开接口全是非虚函数,真正的虚函数全部收在 private 区(libstdc++ 实现,字段略):
class memory_resource {
public:
void* allocate(size_t bytes, size_t alignment = alignof(max_align_t))
{ return do_allocate(bytes, alignment); }
void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t))
{ return do_deallocate(p, bytes, alignment); }
bool is_equal(const memory_resource& other) const noexcept
{ return do_is_equal(other); }
private:
virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
virtual void do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
}; 派生类要做的不是"访问"基类的 private 函数,而是按签名提供自己的 override。override 检查只看函数签名与 cv 限定,不看访问性——派生类的实现可以放在自己的任意访问区。
这个结构是 NVI(Non-Virtual Interface,非虚接口)惯用法的标准形态:公开的非虚函数承担接口与不变式(参数校验、统计、日志),具体的差异行为下沉到 private 虚函数作为定制点。外界只能通过 allocate() 进入,无法绕过公开层的约定直接调用 do_allocate。
验证:多态不受访问性影响
#include <iostream>
class Base {
public:
virtual ~Base() = default;
void test1() { // 公开入口,内部依次调三个虚函数
func1();
func2();
func3();
}
virtual void test() = 0;
protected:
virtual void func1() = 0;
private:
virtual void func2() = 0;
virtual void func3() { std::cout << "Base::func3\n"; }
};
class Derived : public Base {
public:
void test() override {
func1(); // protected,派生类可见
func2(); // Derived 自己的 override,可调
// func3(); // 编译错误:func3 在 Base 中是 private
}
protected:
void func1() override { std::cout << "Derived::func1\n"; }
private:
void func2() override { std::cout << "Derived::func2\n"; }
};
class DDerived : public Derived {
public:
void test() override {
func1();
// func2(); // 编译错误:func2 在 Derived 中是 private
// func3(); // 编译错误:func3 在 Base 中是 private
}
protected:
void func1() override { std::cout << "DDerived::func1\n"; }
};
int main() {
Base* p1 = new Derived;
Base* p2 = new DDerived;
p1->test1();
p2->test1();
delete p1;
delete p2;
} 输出:
Derived::func1
Derived::func2
Base::func3
DDerived::func1
Derived::func2
Base::func3 三个观察:
test1()通过基类指针调用,func2虽然在两个类中都是 private,仍稳定分派到运行时类型的实现——多态完全正常;DDerived没有重写func2,虚表沿继承链找到Derived的实现,输出Derived::func2;- 访问性挡住的只有派生类内部的直接调用:
DDerived::test()里不能调func2()(Derived 的 private),也不能调func3()(Base 的 private),但这不影响任何通过基类指针发生的虚分派。
结论
虚函数的访问性(public/protected/private)只参与访问检查,不参与虚表分派。把纯虚函数声明为 private 的动机通常是 NVI:基类用公开非虚函数固化调用流程,把可定制的步骤藏在 private 虚函数里,既保证外界走统一入口,也给派生类留出 override 的自由。判断某个虚函数该不该 private,与多态机制无关,只取决于这个函数是否应该成为对外(或对派生类)的调用入口。
参考
- C++ 为什么会有 private virtual 函数,这样写好吗? - V2EX
- What is the point of a private pure virtual function? - Stack Overflow
- 《Effective C++(第三版)》条款 35(NVI 惯用法)